package ar.edu.itba.it.pdc.configuration;

import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;

import ar.edu.itba.it.pdc.configuration.models.Comparators;
import ar.edu.itba.it.pdc.configuration.models.ProxyConfiguration;
import ar.edu.itba.it.pdc.configuration.models.filters.destinyFilters.DestinyAllFilter;
import ar.edu.itba.it.pdc.configuration.models.filters.destinyFilters.DestinyFilter;
import ar.edu.itba.it.pdc.configuration.models.filters.destinyFilters.DestinyIpFilter;
import ar.edu.itba.it.pdc.configuration.models.filters.destinyFilters.DestinyIpGroupFilter;
import ar.edu.itba.it.pdc.configuration.models.filters.destinyFilters.DestinyMediaTypeFilter;
import ar.edu.itba.it.pdc.configuration.models.filters.destinyFilters.DestinyResourseSizeFilter;
import ar.edu.itba.it.pdc.configuration.models.filters.destinyFilters.DestinyURIFilter;
import ar.edu.itba.it.pdc.configuration.models.filters.originFilters.OriginBrowserFilter;
import ar.edu.itba.it.pdc.configuration.models.filters.originFilters.OriginFilter;
import ar.edu.itba.it.pdc.configuration.models.filters.originFilters.OriginIpFilter;
import ar.edu.itba.it.pdc.configuration.models.filters.originFilters.OriginIpGroupFilter;
import ar.edu.itba.it.pdc.configuration.models.filters.originFilters.OriginOSFilter;
import ar.edu.itba.it.pdc.nio.TCPProtocol;

public class ConfiguringProtocol implements TCPProtocol{

	private enum OperationType {
		ERROR,
		LISTFILTERS,
		DELETEFILTER,
		ADDFILTER,
		IMAGEROTATIONS,
		L33T,
		GETSTATUS
	}
	
	private enum ProtocolState {
		NAME,
		VERSION,
		OPERATION,
		CONTENT,
		ORIGINTYPE,
		ORIGINCONTENT,
		DESTINYTYPE,
		DESTINYCONTENT
	}
	
	private enum OriginFilterType {
		IP,
		IPGROUP,
		OS,
		BROWSER
	}
	
	private enum DestinyFilterType {
		IP,
		IPGROUP,
		ALL,
		MEDIATYPE,
		SIZE,
		URI
	}
	
	
	private int bufSize;
	private ProxyConfiguration config;
	private double version;
	private String error;
	private OperationType opType;
	private OriginFilter originFilter;
	private DestinyFilter destFilter;
	private int idRule;
	
	public ConfiguringProtocol(int bufSize) {
		this.bufSize = bufSize;
		this.config = ProxyConfiguration.getInstance();
		this.version = Constants.CONFIGVERSION;
	}
	
	@Override
	public void handleAccept(SelectionKey key) throws IOException {
		SocketChannel clntChan = ((ServerSocketChannel) key.channel()).accept();
		clntChan.configureBlocking(false); // Must be nonblocking to register
		// Register the selector with new channel for read and attach byte
		// buffer
		clntChan.register(key.selector(), SelectionKey.OP_READ, null);
	}

	@Override
	public void handleRead(SelectionKey key) throws IOException {
		SocketChannel clntChan = (SocketChannel) key.channel();
		
		List<ByteBuffer> bufs = new ArrayList<ByteBuffer>();
		bufs.add(ByteBuffer.allocate(bufSize));
		
		int i = 0;
		
		clntChan.read(bufs.get(i));
		
		while (!(new String(bufs.get(i).array()).contains("\n"))) { // Did the other end close?
			i++;
			bufs.add(ByteBuffer.allocate(bufSize));
			clntChan.read(bufs.get(i));
		}
		
		this.parseProtocol(bufs);
		key.interestOps(SelectionKey.OP_WRITE);
	}

	private void setError(String error) {
		this.error = error;
	}
	
	private void parseProtocol(List<ByteBuffer> bufs) {
		String content = "";
		ProtocolState protocolState = ProtocolState.NAME;
		OriginFilterType originFilterType = null;
		DestinyFilterType destinyFilterType = null;
		SortedSet<InetAddress> ipsOrig = new TreeSet<InetAddress>(Comparators.getIpComparator());
		SortedSet<InetAddress> ipsDest = new TreeSet<InetAddress>(Comparators.getIpComparator());
		for (ByteBuffer byteBuffer : bufs) {
			String buffer = new String(byteBuffer.array());
			char[] charBuffer = buffer.toCharArray();
			for (int i = 0; i < charBuffer.length; i++) {
				switch (protocolState) {
				case NAME:
					if (charBuffer[i] == '\n') {
						this.opType = OperationType.ERROR;
						setError("Not enough parameters were provided for the configuration protocol.\n" +
								"Remember: config version optype [OPTIONS]");
						return;
					}
					if (charBuffer[i] == ' ') {
						if (!"config".equals(content)) {
							this.opType = OperationType.ERROR;
							setError("In order to access the configuration remember to use the command config.\n" +
								"Remember: config version optype [OPTIONS]\n");
							return;
						}
						protocolState = ProtocolState.VERSION;
						content = "";
					} else {
						content = content + charBuffer[i];
					}
					break;
				case VERSION:
					if (charBuffer[i] == '\n') {
						this.opType = OperationType.ERROR;
						setError("Not enough parameters were provided for the configuration protocol.\n" +
								"Remember: config version optype [OPTIONS]\n");
						return;
					}
					if (charBuffer[i] == ' ') {
						double dVersion = 0;
						try {
							dVersion = Double.parseDouble(content);
						} catch (Exception e) {
							this.opType = OperationType.ERROR;
							setError("This version of the protocol is not supported.\n" +
									"Remember: config version optype [OPTIONS]\n");
							return;
						}
						if (dVersion > this.version) {
							this.opType = OperationType.ERROR;
							setError("This version of the protocol is not supported.\n" +
									"Remember: config version optype [OPTIONS]\n");
							return;
						}
						protocolState = ProtocolState.OPERATION;
						content = "";
					} else {
						content = content + charBuffer[i];
					}
					break;
				case OPERATION:
					int operation;
					if (charBuffer[i] == '\n') {
						operation = parseOperation(content);
						switch (operation) {
						case 1:
							this.opType = OperationType.LISTFILTERS;
							return;
						case 2:
							this.opType = OperationType.ERROR;
							setError("Not enough parameters were provided for the configuration protocol.\n" +
								"Remember: config version optype [OPTIONS]\n" + 
								"In this case: config version 2 idRule\n");
							return;
						case 3:
							this.opType = OperationType.ERROR;
							protocolState = ProtocolState.ORIGINTYPE;
							setError("Not enough parameters were provided for the configuration protocol.\n" +
									"Remember: config version optype [OPTIONS]\n" + 
									"In this case: config version 3 clientRuleType clientRule serverRuleType serverRule");
							return;
						case 4:
							this.opType = OperationType.IMAGEROTATIONS;
							return;
						case 5:
							this.opType = OperationType.L33T;
							return;
						case 6:
							this.opType = OperationType.GETSTATUS;
							return;
						default:
							return;
						}
					}
					if (charBuffer[i] == ' ') {
						operation = parseOperation(content);
						switch (operation) {
						case 1:
							this.opType = OperationType.LISTFILTERS;
							return;
						case 2:
							this.opType = OperationType.DELETEFILTER;
							protocolState = ProtocolState.CONTENT;
							break;
						case 3:
							this.opType = OperationType.ADDFILTER;
							protocolState = ProtocolState.ORIGINTYPE;
							break;
						case 4:
							this.opType = OperationType.IMAGEROTATIONS;
							return;
						case 5:
							this.opType = OperationType.L33T;
							return;
						case 6:
							this.opType = OperationType.GETSTATUS;
							return;
						default:
							return;
						}
						content = "";
					} else {
						content = content + charBuffer[i];
					}
					break;
				case CONTENT:
					if (charBuffer[i] == '\n') {
						try {
							idRule = Integer.parseInt(content);
						} catch (Exception e) {
							this.opType = OperationType.ERROR;
							setError("The id of a rule must be a number.\n" +
									"Remember: config version optype [OPTIONS]\n" +
									"In this case: config version 2 idRule\n");
							return;
						}
						return;
					} else {
						content = content + charBuffer[i];
					}
					break;
				case ORIGINTYPE:
					if (charBuffer[i] == '\n') {
						this.opType = OperationType.ERROR;
						setError("Not enough parameters were provided for the configuration protocol.\n" +
								"Remember: config version optype [OPTIONS]\n" + 
								"In this case: config version 3 clientRuleType clientRule serverRuleType serverRule");
						return;
					}
					if (charBuffer[i] == ' ') {
						int originType;
						try {
							originType = Integer.parseInt(content);
						} catch (Exception e) {
							this.opType = OperationType.ERROR;
							setError("The clientRuleType must be a number.\n" +
									"Remember: config version optype [OPTIONS]\n" + 
									"In this case: config version 3 clientRuleType clientRule serverRuleType serverRule" +
									"Client Rule Type #1 = IP\n" +
									"Client Rule Type #2 = IP Group\n" +
									"Client Rule Type #3 = OS\n" +
									"Client Rule Type #4 = Browser\n");
							return;
						}
						if (originType > 4 || originType < 1) {
							this.opType = OperationType.ERROR;
							setError("The clientRuleType must be a number.\n" +
									"Remember: config version optype [OPTIONS]\n" + 
									"In this case: config version 3 clientRuleType clientRule serverRuleType serverRule" +
									"Client Rule Type #1 = IP\n" +
									"Client Rule Type #2 = IP Group\n" +
									"Client Rule Type #3 = OS\n" +
									"Client Rule Type #4 = Browser\n");
							return;
						}
						switch (originType) {
						case 1:
							originFilterType = OriginFilterType.IP;
							break;
						case 2:
							originFilterType = OriginFilterType.IPGROUP;
							break;
						case 3:
							originFilterType = OriginFilterType.OS;
							break;
						case 4:
							originFilterType = OriginFilterType.BROWSER;
							break;
						default:
							break;
						}
						protocolState = ProtocolState.ORIGINCONTENT;
						content = "";
					} else {
						content = content + charBuffer[i];
					}
					break;
				case ORIGINCONTENT:
					switch (originFilterType) {
					case IP:
						if (charBuffer[i] == '\n') {
							this.opType = OperationType.ERROR;
							setError("Not enough parameters were provided for the configuration protocol.\n" +
									"Remember: config version optype [OPTIONS]\n" + 
									"In this case: config version 3 1 ip serverRuleType serverRule\n");
							return;
						}
						if (charBuffer[i] == ' ') {
							InetAddress ip = null;
							try {
								ip = InetAddress.getByName(content);
							} catch (UnknownHostException e) {
								this.opType = OperationType.ERROR;
								setError("The Ip of the client provided was not valid.\n" +
									"Remember: config version optype [OPTIONS]\n" + 
									"In this case: config version 3 1 ip serverRuleType serverRule\n");
								return;
							}
							originFilter = new OriginIpFilter(ip);
							protocolState = ProtocolState.DESTINYTYPE;
							content = "";
						} else {
							content = content + charBuffer[i];
						}
						break;
					case IPGROUP:
						if (charBuffer[i] == '\n') {
							this.opType = OperationType.ERROR;
							setError("Not enough parameters were provided for the configuration protocol.\n" +
									"Remember: config version optype [OPTIONS]\n" + 
									"In this case: config version 3 2 ips serverRuleType serverRule\n" +
									"Remember that ips must be separated by commas\n");
							return;
						}
						if (charBuffer[i] == ' ') {
							InetAddress ip = null;
							try {
								ip = InetAddress.getByName(content);
							} catch (UnknownHostException e) {
								this.opType = OperationType.ERROR;
								setError("One of the Ip's of the client provided was not valid.\n" +
									"Remember: config version optype [OPTIONS]\n" + 
									"In this case: config version 3 2 ips serverRuleType serverRule\n" + 
									"Remember that ips must be separated by commas\n");
								return;
							}
							ipsOrig.add(ip);
							originFilter = new OriginIpGroupFilter(ipsOrig);
							protocolState = ProtocolState.DESTINYTYPE;
							content = "";
						} else if (charBuffer[i] == ',') {
							InetAddress ip = null;
							try {
								ip = InetAddress.getByName(content);
							} catch (UnknownHostException e) {
								this.opType = OperationType.ERROR;
								setError("One of the Ip's of the client provided was not valid.\n" +
										"Remember: config version optype [OPTIONS]\n" + 
										"In this case: config version 3 2 ips serverRuleType serverRule\n" + 
										"Remember that ips must be separated by commas\n");
								return;
							}
							ipsOrig.add(ip);
							content = "";
						} else {
							content = content + charBuffer[i];
						}
						break;
					case BROWSER:
						if (charBuffer[i] == '\n') {
							this.opType = OperationType.ERROR;
							setError("Not enough parameters were provided for the configuration protocol.\n" +
									"Remember: config version optype [OPTIONS]\n" + 
									"In this case: config version 3 4 browser serverRuleType serverRule");
							return;
						}
						if (charBuffer[i] == ' ') {
							originFilter = new OriginBrowserFilter(content);
							protocolState = ProtocolState.DESTINYTYPE;
							content = "";
						} else {
							content = content + charBuffer[i];
						}
						break;
					case OS:
						if (charBuffer[i] == '\n') {
							this.opType = OperationType.ERROR;
							setError("Not enough parameters were provided for the configuration protocol.\n" +
									"Remember: config version optype [OPTIONS]\n" + 
									"In this case: config version 3 3 OS serverRuleType serverRule");
							return;
						}
						if (charBuffer[i] == ' ') {
							originFilter = new OriginOSFilter(content);
							protocolState = ProtocolState.DESTINYTYPE;
							content = "";
						} else {
							content = content + charBuffer[i];
						}
						break;
					default:
						break;
					}
					break;
				case DESTINYTYPE:
					if (charBuffer[i] == '\n') {
						if ("3".equals(content)) {
							destFilter = new DestinyAllFilter();
							return;
						}
						this.opType = OperationType.ERROR;
						setError("Not enough parameters were provided for the configuration protocol.\n" +
								"Remember: config version optype [OPTIONS]\n" + 
								"In this case: config version 3 clientRuleType clientRule serverRuleType serverRule\n" +
								"Note: If serverRuleType is 3 (All), there is no need for serverRule");
						return;
					}
					if (charBuffer[i] == ' ') {
						int originType;
						try {
							originType = Integer.parseInt(content);
						} catch (Exception e) {
							this.opType = OperationType.ERROR;
							setError("The clientRuleType must be a number.\n" +
									"Remember: config version optype [OPTIONS]\n" + 
									"In this case: config version 3 clientRuleType clientRule serverRuleType serverRule" +
									"Server Rule Type #1 = IP\n" +
									"Server Rule Type #2 = IP Group\n" +
									"Server Rule Type #3 = All\n" +
									"Server Rule Type #4 = Media Type\n" +
									"Server Rule Type #5 = Size" +
									"Server Rule Type #6 = URI\n");
							return;
						}
						if (originType > 6 || originType < 1) {
							this.opType = OperationType.ERROR;
							setError("The clientRuleType must be a number.\n" +
									"Remember: config version optype [OPTIONS]\n" + 
									"In this case: config version 3 clientRuleType clientRule serverRuleType serverRule" +
									"Server Rule Type #1 = IP\n" +
									"Server Rule Type #2 = IP Group\n" +
									"Server Rule Type #3 = All\n" +
									"Server Rule Type #4 = Media Type\n" +
									"Server Rule Type #5 = Size" +
									"Server Rule Type #6 = URI\n");
							return;
						}
						switch (originType) {
						case 1:
							destinyFilterType = DestinyFilterType.IP;
							break;
						case 2:
							destinyFilterType = DestinyFilterType.IPGROUP;
							break;
						case 3:
							destFilter = new DestinyAllFilter();
							return;
						case 4:
							destinyFilterType = DestinyFilterType.MEDIATYPE;
							break;
						case 5:
							destinyFilterType = DestinyFilterType.SIZE;
							break;
						case 6:
							destinyFilterType = DestinyFilterType.URI;
						default:
							break;
						}
						protocolState = ProtocolState.DESTINYCONTENT;
						content = "";
					} else {
						content = content + charBuffer[i];
					}
					break;
				case DESTINYCONTENT:
					switch (destinyFilterType) {
					case IP:
						if (charBuffer[i] == '\n') {
							InetAddress ip = null;
							try {
								ip = InetAddress.getByName(content);
							} catch (UnknownHostException e) {
								this.opType = OperationType.ERROR;
								setError("The Ip of the server provided was not valid.\n" +
									"Remember: config version optype [OPTIONS]\n" + 
									"In this case: config version 3 clientRuleType clientRule 1 ip\n");
								return;
							}
							destFilter = new DestinyIpFilter(ip);
							return;
						} else {
							content = content + charBuffer[i];
						}
						break;
					case IPGROUP:
						if (charBuffer[i] == '\n') {
							InetAddress ip = null;
							try {
								ip = InetAddress.getByName(content);
							} catch (UnknownHostException e) {
								this.opType = OperationType.ERROR;
								setError("One of the Ip's of the server provided was not valid.\n" +
									"Remember: config version optype [OPTIONS]\n" + 
									"In this case: config version 3 clientRuleType clientRule 2 ips\n" + 
									"Remember that ips must be separated by commas\n");
								return;
							}
							ipsDest.add(ip);
							destFilter = new DestinyIpGroupFilter(ipsDest);
							return;
						} else if (charBuffer[i] == ',') {
							InetAddress ip = null;
							try {
								ip = InetAddress.getByName(content);
							} catch (UnknownHostException e) {
								this.opType = OperationType.ERROR;
								setError("One of the Ip's of the server provided was not valid.\n" +
										"Remember: config version optype [OPTIONS]\n" + 
										"In this case: config version 3 clientRuleType clientRule 2 ips\n" + 
										"Remember that ips must be separated by commas\n");
							}
							ipsDest.add(ip);
							content = "";
						} else {
							content = content + charBuffer[i];
						}
						break;
					case MEDIATYPE:
						if (charBuffer[i] == '\n') {
							destFilter = new DestinyMediaTypeFilter(content);
							return;
						} else {
							content = content + charBuffer[i];
						}
						break;
					case SIZE:
						if (charBuffer[i] == '\n') {
							int size;
							try {
								size = Integer.parseInt(content);
							} catch (Exception e) {
								this.opType = OperationType.ERROR;
								setError("The allowed size of a resource must be a number.\n" +
										"In this case: config version 3 clientRuleType clientRule 5 size\n");
								return;
							}
							destFilter = new DestinyResourseSizeFilter(size);
							return;
						}
						content = content + charBuffer[i];
						break;
					case URI:
						if (charBuffer[i] == '\n') {
							destFilter = new DestinyURIFilter(content);
							return;
						} else {
							content = content + charBuffer[i];
						}
						break;
					default:
						break;
					}
					break;
				default:
					break;
				}
			}
		}
	}

	@Override
	public void handleWrite(SelectionKey key) throws IOException {
		
		SocketChannel clntChan = (SocketChannel) key.channel();
		
		String output = null;
		switch (opType) {
		case ERROR:
			output = "ERROR:\n" + getError();
			break;
		case LISTFILTERS:
			output = config.printRules(); 
			break;
		case DELETEFILTER:
			boolean success = config.deleteFilterRule(idRule);
			if (success) {
				output = "Rule #" + idRule + " was succesfully deleted\n";
			} else {
				output = "Rule #" + idRule + " doesn't exist";
			}
			break;
		case ADDFILTER:
			config.addFilterRule(originFilter, destFilter);
			output = "Rule seccesfully added\n";
			break;
		case IMAGEROTATIONS:
			config.rotateImages();
			output = "Image rotation is now set in " + config.isRotateImages();  
			break;
		case L33T:
			config.l33t();
			output = "L33t transformation is now set in " + config.isL33t();
			break;
		case GETSTATUS:
			output = config.printConfig();
			break;
		default:
			break;
		}
		
		ByteBuffer buf;
		int len = output.length();
		int limit;
		
		for (int i = 0; i < len; i = i + bufSize) {
			limit = (len > i + bufSize) ? i + bufSize : len;
			buf = ByteBuffer.wrap((output.substring(i, limit) + "\n").getBytes());
			
			clntChan.write(buf);
		}
		
		clntChan.close();
	}
	
	private String getError() {
		return error;
	}

	private int parseOperation(String content) {
		int operation;
		try {
			operation = Integer.parseInt(content);
		} catch (Exception e) {
			this.opType = OperationType.ERROR;
			setError(getErrorOpType());
			return -1;
		}
		if (operation > 6 || operation < 1) {
			this.opType = OperationType.ERROR;
			setError(getErrorOpType());
			return -1;
		}
		return operation;
	}
	
	private String getErrorOpType() {
		return "This operation is not supported.\n" +
				"Remember: config version optype [OPTIONS]\n" +
				"optype MUST be a number.\n" + 
				"optype #1 = List the current filters.\n" +
				"optype #2 = Delete a filter.\n" +
				"optype #3 = Add a Filter.\n" +
				"optype #4 = Change the image rotations configuration.\n" +
				"optype #5 = Change the l33t transformation configuration.\n" +
				"optype #6 = Print the current configurations.\n";
	}
}
